import {BridgeHostEvents, IRpcBridgeHost, RpcBridge, RpcBridgeTrafficHook} from "./RpBridge";
import {v4 as uuid} from "uuid"
import {IStatefulClientInterface} from "./ProtocolInterface";
import { Completer, RpcMessage, RpcRequest } from "./RpBridge";

/**
 Created by:
 muhtearjan mehmood مۇختەرجان مەخمۇت (Email: yeganaaa@163.com)
 At 8/14/20 12:55 PM
 */

 /**
  * Server host events must be called by RpcServerHost implementations
  */
export class ServerHostEvents {
    /**
     * Create an instance
     * @param onReady Must be call by IRpcServerHost instance when it is ready
     * @param onClientConnected Must call by IRpcServerHost instance when a client is connected
     * @param onError Must call by IRpcServerHost instance when error occurred
     * @param onShutdown Must call by IRpcServerHost instance when server is shutdown
     */
    constructor(public onReady: () => Promise<void>, public onClientConnected: (bridge: IRpcBridgeHost<any>) => Promise<void>, public onError: (error: Error) => Promise<void>, public onShutdown: () => Promise<void>) {
    }
}
/**
 * Standards of RpcServerHost,
 * @remarks you must be implement this interface when you define your own RpcServerHost like "WebSocketRpcServerHost", "TcpSocketRpcServerHost", "HttpLongPoolingRpcServerHost"
 */
export interface IRpcServerHost<BACKEND> {
    /**
     * backend object underlying this server
     * @remarks This is not required and this is not used by anywhere except this interface instances
     */
    backend: BACKEND
    /**
     * Call this method before rpc server prepare for start
     * @param eventHandler handler object supplied by RpcServer class
     */
    prepare(eventHandler: ServerHostEvents): Promise<void>
    /**
     * Call this method when RpcServer is want to launch
     */
    launch(): Promise<void>
    /**
     * Call this method when RpcServer is want to shutdown
     */
    shutdown(): Promise<void>
}
/**
 * State of RpcClient
 * @remarks
 * When a client disconnected from rpc server then this object is saved to someplace by StateManager object, this is corresponds to HttpServer session.
 */
export abstract class State {
    /**
     * Id of this state
     */
    id: string = uuid()
}
/**
 * Client state
 */
export enum ClientState {
    /**
     * Disconnected state
     */
    DISCONNECTED = 0x01,
    /**
     * Connected state
     */
    CONNECTED = 0x02,
}
/**
 * An intermediate host implementation (actually not real implementation) for RpcServer for determine client is disconnecting.
 */
export class IntermediateBridgeHost implements IRpcBridgeHost<any>{
    backend: any;
    constructor(private sourceHost: IRpcBridgeHost<any>, private before: BridgeHostEvents, private after: BridgeHostEvents) {
        this.backend = this.sourceHost.backend
    }

    public close(): Promise<void> {
        return this.sourceHost.close()
    }

    open(): Promise<void> {
        return this.sourceHost.open()
    }

    prepare(eventHandler: BridgeHostEvents): Promise<void> {
        const handlers = [this.before, eventHandler, this.after]
        return this.sourceHost.prepare(new BridgeHostEvents(
            async () => {
                for (const action of handlers.map(value => value.onReady)) await action()
            },
            async error => {
                for (const action of handlers.map(value => value.onError)) await action(error)
            },
            async () => {
                for (const action of handlers.map(value => value.onClose)) await action()
            },
            async () => {
                for (const action of handlers.map(value => value.onUnableToConnect)) await action()
            },
            async data => {
                for (const action of handlers.map(value => value.onMessage)) await action(data)
            },
        ))
    }

    send(data: Uint8Array): Promise<void> {
        return this.sourceHost.send(data)
    }
}
/**
 * An empty function that do nothing.
 */
async function emptyFun() { }
type RequestHook = (message: RpcRequest, action: (message: RpcRequest) => Promise<void>) => Promise<void>
class CustomHook extends RpcBridgeTrafficHook {
    constructor (private rpcCallRequestHook: RequestHook) {
        super()
    }
    public callRequest(message: RpcRequest, action: (message: RpcRequest) => Promise<void>) {
        return this.rpcCallRequestHook(message, action)
    }
}
/**
 * Standards of RpcServer
 * @remarks
 * First of all, This is the another one of core class in this framework,
 * Rpc client is should be extend this class, This class is corresponds HttpClient in http applications,
 * This object is holds underlying {@link RpcBridge} object and State
 */
export abstract class RpcClient<LOCAL_INTERFACE, REMOTE_INTERFACE extends IStatefulClientInterface, STATE extends State> {
    public id: string = uuid()
    public rpcState: ClientState = ClientState.DISCONNECTED
    /**State object */
    public state: STATE = null!
    /**Underlying RpcBridge object represent to remote side */
    public bridge: RpcBridge<LOCAL_INTERFACE, REMOTE_INTERFACE> = null!
    /**Rpc server object that holds this client */
    public server: RpcServer<LOCAL_INTERFACE, REMOTE_INTERFACE, STATE> = null!
    /**State ready completer */
    public stateReadyCompleter = Completer.create()
    /**
     * Called when new client is connected
     * @remarks
     * When override this method, you MUST call this method in your own overrides using "super.onClientConnected()"
     */
    protected async onConnected(): Promise<void> {}
    /**
     * Create state object instance when RpcClient is cannot find this client's state in StateManager.
     */
    protected abstract createState(): Promise<STATE>
    /**
     * Get local interface implementation used by remote side.
     */
    protected abstract getLocalInterfaceImplementation(): Promise<LOCAL_INTERFACE>
    /**
     * Called when this client's state is restored from StateManager to this client.
     * @summary
     * When override this method, you MUST call this method in your own overrides using "super.restoreState(state)"
     * @param state state object
     * @mustSuperCall
     */
    protected async restoreState(state: STATE): Promise<void> {this.state = state}
    /**
     * Called when this client is ready
     * @remarks
     * This time state is already restored or created and now ready to use.
     * @summary
     * When override this method, you MUST call this method in your own overrides using "super.restoreState()"
     * @mustSuperCall
     */
    protected async onReady(): Promise<void> {}
    /**
     * Call when client is disconnected and this client attempt to save it's state
     * @summary
     * When override this method, you MUST call this method in your own overrides using "super.saveState(stateManager)"
     * @param stateManager state manager
     * @mustSuperCall
     */
    protected async saveState(stateManager: IStateManager<STATE>): Promise<void> {await stateManager.save(this.state)}
    /**
     * Call when client is disconnected
     * * @summary
     * When override this method, you MUST call this method in your own overrides using "super.saveState(stateManager)"
     * @mustSuperCall
     */
    protected async onDisconnected(): Promise<void> {}
    /**
     * Call this method by RpcServer while this client is begin initializing.
     * @summary
     * User MUST DO NOT call this method anywhere, this method only called by RpcServer
     * @param bridgeHost bridge host of this client will be used.
     */
    public async attach(bridgeHost: IRpcBridgeHost<any>) {
        bridgeHost = new IntermediateBridgeHost(
            bridgeHost,
            new BridgeHostEvents(
                emptyFun,
                emptyFun,
                emptyFun,
                emptyFun,
                emptyFun,
            ),
            new BridgeHostEvents(
                emptyFun,
                emptyFun,
                async () => {await this.detach()},
                emptyFun,
                emptyFun
            )
        )
        this.bridge = new RpcBridge<LOCAL_INTERFACE, REMOTE_INTERFACE>(bridgeHost, await this.getLocalInterfaceImplementation())
        this.bridge.hook.receiving = new CustomHook(async (message, action) => {
            await this.stateReadyCompleter.promise
            await action(message)
        })
        this.server.clients.set(this.id, this)
        await this.bridge.connect()
        this.rpcState = ClientState.CONNECTED
        await this.onConnected()
        await this.resolveState()
        await this.onReady()
        this.stateReadyCompleter.complete()
    }
    /**
     * Resolve client state (restore from state manager or create new)
     */
    private async resolveState() {
        const stateKey = await this.bridge.remoteInterface.stateModule.getStateKey() ?? ""
        const hasState = await this.server.stateManager.has(stateKey)
        if (hasState) {
            const state = await this.server.stateManager.get(stateKey)
            this.bridge.remoteInterface.stateModule.stateRestored(stateKey)
            await this.restoreState(state)
        } else {
            const state = await this.createState()
            await this.server.stateManager.save(state)
            this.bridge.remoteInterface.stateModule.setStateKey(stateKey)
            this.state = state
        }
    }
    /**
     * Detach client object from rpc server
     * Calling after client is disconnected, this method is used by inner of this class and MUST DO NOT call anywhere
     */
    private async detach() {
        await this.stateReadyCompleter.promise
        this.rpcState = ClientState.DISCONNECTED
        await this.saveState(this.server.stateManager)
        this.server.clients.delete(this.id)
        await this.onDisconnected()
    }
}
/**
 * State management
 * @remarks
 * State management is responsible to manage clients state,
 * users can be define own state management implementations by implementing this interface like "RedisStateManagement", "MongoDBStateManagement", "inMemoryStateManagement"
 */
export interface IStateManager<STATE extends State> {
    /**
     * Rpc server instance
     */
    server: RpcServer<any, any, any>
    /**
     * Initialize this StateManagement
     */
    initialize(): Promise<void>
    /**
     * Dispose this state management
     * @remarks
     * at this time maybe this state management object saves it's contents to someplace (this is dependent on actual implementation)
     */
    dispose(): Promise<void>
    /**
     * Has this state
     * @param stateId id of state
     */
    has(stateId: string): Promise<boolean>
    /**
     * Get state by id
     * @param stateId id of state
     */
    get(stateId: string): Promise<STATE>
    /**
     * Save this state to this state manager
     * @param state id of state
     */
    save(state: STATE): Promise<void>
    /**
     * Get all states contains this state manager
     */
    getAll(): Promise<STATE[]>
    /**
     * Clear given state of id
     * @param stateId id of state
     */
    clear(stateId: string): Promise<boolean>
    /**
     * Clear all state saved in this state management
     */
    clearAll(): Promise<void>
}
/**
 * Standards of RpcServer
 * @remarks
 * First of all, This is the another one of core class in this framework,
 * Rpc server is should be extend this class, This class is corresponds HttpServer in http applications,
 * This object is handles and saves all connected RpcClient instances, StateManager instances
 */
export abstract class RpcServer<LOCAL_INTERFACE, REMOTE_INTERFACE extends IStatefulClientInterface, STATE extends State> {
    /**
     * create instance
     * @param serverHost RpcServerHost instances underlying this RpcServer.
     * @param stateManager StateManager instance that responsible manage connected clients state 
     */
    public constructor(
        protected serverHost: IRpcServerHost<any>,
        public stateManager: IStateManager<STATE>
    ) {
        stateManager.server = this
    }
    /**
     * All connected Clients
     */
    clients: Map<string, RpcClient<LOCAL_INTERFACE, REMOTE_INTERFACE, STATE>> = new Map()
    /**
     * Create client instance when RpcServer accepts a client connection
     */
    protected abstract createClientInstance(): Promise<RpcClient<LOCAL_INTERFACE, REMOTE_INTERFACE, STATE>>
    /**
     * Prepare this RpcServer for launch
     * @remarks
     * During thi time, the server registers it's events to ServerHost and prepare for launch.
     */
    public async prepareForLaunch() {
        await this.stateManager.initialize()
        const handler = new ServerHostEvents(
            async () => {await this.onLaunch()},
            async bridge => {
                const clientInstance = await this.createClientInstance()
                clientInstance.server = this
                await clientInstance.attach(bridge)
            },
            async error => {await this.onError(error)},
            async () => {await this.onShutdown()}
        )
        await this.serverHost.prepare(handler)
    }
    /**
     * Launch this rpc server
     */
    public async launch() {await this.serverHost.launch()}
    /**
     * Called when Server is started
     * @summary
     * Please call super method using "super.onLaunch()" to call super method if you override this lifecycle method.
     * @mustCallSuper
     */
    protected async onLaunch() {}
    /**
     * Shutdowns this rpc server and underlying {@link IRpcServerHost}
     */
    public async shutdown() {
        await this.serverHost.shutdown()
        for (const client of this.clients.values()) {
            try {
                await client.bridge.disconnect()
            }
            catch (error) {}
        }
        this.clients.clear()
    }
    /**
     * Called when server is stopped
     * @summary
     * Please call super method using "super.onLaunch()" to call super method if you override this lifecycle method.
     * @mustCallSuper
     */
    protected async onShutdown() {await this.stateManager.dispose()}
    /**
     * Called when an error occurred on underlying RpcServerHost's connection.
     * @summary
     * Please call super method using "super.onError(error)" to call super method if you override this lifecycle method.
     * @param error an instance of Error
     * @mustCallSuper
     */
    protected async onError(error: Error){console.error(error)}
}
